#pragma once
#include "Config.h"
#include "Uniform.h"

class Shader {
public:
	enum _VBO_ { NumVBOs = 5 };
	enum _TYPE_ { TYPE_VERTEX, TYPE_FRAGMENT, TYPE_GEOMETRY, TYPE_PROGRAM };

	Shader() = default;

	~Shader() {
		if (!isValid())
			return;
		if (m_isVABOCacheEnabled)
			return;
		if (m_isBufObjectCreated)
			destroyBufObjects();
	}

	void install(const CString &shaderName);

	Uniform& uniformRef() {
		if (!m_uniform.isCreated())
			m_uniform = Uniform(m_progID);
		return m_uniform;
	}

	static void resetGL() { ShaderProgramMap.clear(); }

	/** 
	* @brief switch between different shader programs; activate the shader; at the moment when loop run, call it
	* @warning don't forget to enable shader(call use()) before setting uniforms
	* @warning note abuse will leads to ongoing rendering and setting async problems! call it in a unified place.
	*/
	void use() {
		if (!isValid())
			EXIT("Shader::use -> shader is not installed!!!");
		glUseProgram(m_progID);
		CurrentUsingProgram = m_progID;
		CurrentUsingProgramShaderName = m_shaderName;
	}

	bool isValid() { return InvalidNum != m_progID; }

	/**
	* @brief If enabled, the values in the generic vertex attribute array will be accessed and used for rendering when calls are made to vertex array commands
	*        such as glDrawArrays, glDrawArraysInstanced, glDrawElements, glDrawElementsInstanced, or glDrawRangeElements.
	* @note VAO's data is coming from VBO, so it is associated with VBO
	* @note glBufferData: Copy data from CPU memory to GPU memory. so call it standalone.
	* @note because of 'Automatic type inference', can ignore template type <T> when call
	* @param datas because of Copy, it can be local variable
	* @param attribName Related to GPU/GLSL{ layout (location = [attribIndex]) in [value_type] [`attribName`] }
	* @param numComponentsPerVertex Specifies the number of components per generic vertex attribute. Must be 1, 2, 3, 4. Example: vertex = 3, texture coord = 2, pure value = 1
	*/
	template <typename T>
	void prepareVABO(GLSLVarName attribName, const CVector<T>& attribData, GLint numComponentsPerVertex = sizeof(T) / sizeof(GL_FLOAT), GLenum componentType = GL_FLOAT) {
		prepareVABO(Enum_toStr(attribName), attribData, numComponentsPerVertex, componentType);
	}

	template <typename T>
	void prepareVABO(const char* attribName, const CVector<T>& attribData, GLint numComponentsPerVertex = sizeof(T) / sizeof(GL_FLOAT), GLenum componentType = GL_FLOAT) {
		if (attribData.empty())
			return;
		if (!m_isBufObjectCreated) {
			createBufObjects();// each has own buffers
			m_isBufObjectCreated = true;
		}
		copyVBODataToGPU(m_vboCnt++, attribData);

		GLint attribLocation = glGetAttribLocation(m_progID, attribName);
		glVertexAttribPointer(attribLocation, numComponentsPerVertex, componentType, GL_FALSE, NULL, NULL);// args: (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void * pointer)
		glEnableVertexAttribArray(attribLocation);// enable once for each attribIndex

		if (Enum_toStr(GLSLVarName::Vertex) == attribName)
			m_vertexNum = attribData.size();
		else if (Enum_toStr(GLSLVarName::InstancedVertex) == attribName) {
			glVertexAttribDivisor(attribLocation, 1);CHECK_GL_ERR// modify the rate at which generic vertex attributes advance during instanced rendering
			m_drawInstancedNum = attribData.size();
		}
	}

	void prepareEBO(const CVector<GLuint>& indices) {
		if (indices.empty())
			return;
		copyIndexDataToGPU(indices);
		
		m_indexNum = indices.size();
	}

	/**
	* @note render multi entity once, should switch-binding-datas and uniform variables each!
	* @note do not glFlush for each shape, call glFlush when all drawing has been completed
	*/
	void draw(PrimitiveType primitiveType) {
		GLenum mode = GL_TRIANGLES;
		switch (primitiveType) {
			case PrimitiveType::TRIANGLE_STRIP: mode = GL_TRIANGLE_STRIP; break;
			case PrimitiveType::TRIANGLE_FAN: mode = GL_TRIANGLE_FAN; break;
			case PrimitiveType::POINTS: mode = GL_POINTS; break;
			default: break;
		}
		glBindVertexArray(m_vao[0]);
		for (int i = 0; i < m_vboCnt; i++)
			glBindBuffer(GL_ARRAY_BUFFER, m_vbo[i]);

		if (m_drawInstancedNum > 0) {
			glDrawArraysInstanced(mode, 0, m_vertexNum, m_drawInstancedNum);CHECK_GL_ERR
			return;
		}

		if (m_indexNum == 0) {
			glDrawArrays(mode, 0, m_vertexNum);
		} else {
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo[0]);
			glDrawElements(mode, m_indexNum, GL_UNSIGNED_INT, NULL);
		}
	}

	void enableVABOCache() { m_isVABOCacheEnabled = true; }

private:
	GLuint m_progID = InvalidNum;
	CString m_shaderName;

	static CMap<CString, GLuint> ShaderProgramMap;

	Uniform m_uniform;

	bool m_isBufObjectCreated = false;

	GLuint m_vao[1], m_vbo[NumVBOs], m_ebo[1];// to ensure every entity has their own vao/vbo/ebo, not shared as global

	int m_vboCnt = 0;

	int m_vertexNum, m_drawInstancedNum = 0, m_indexNum = 0;

	bool m_isVABOCacheEnabled = false;

	/**
	 - VAO: Vertex Array Object. used to store the state of vertex attributes
	 - VBO: Vertex Buffer Object
	 - EBO: Element(Index) Buffer Object
	 - FBO: Framebuffer Object
	 - PBO: Pixel Buffer Object
	 * @warning Because all states are reset each frame, only a single VAO is used, which increases the cost of state switching and may affect performance.
	*/
	void createBufObjects() {
		glGenVertexArrays(1, m_vao);// VAO
		glBindVertexArray(m_vao[0]);// bind once for each entity

		glGenBuffers(NumVBOs, m_vbo);// VBO
		glGenBuffers(1, m_ebo);// EBO
	}

	void destroyBufObjects() {
		// unbind
		glBindVertexArray(NULL);
		glBindBuffer(GL_ARRAY_BUFFER, NULL);
		// delete(GPU part)
		glDeleteVertexArrays(1, m_vao);
		glDeleteBuffers(NumVBOs, m_vbo);
		glDeleteBuffers(1, m_ebo);
	}

	void deleteTextures(GLsizei n, const GLuint* textures) {
		glDeleteTextures(n, textures);
	}
	
	/**
	 * @note glBufferData: Copy data from CPU memory to GPU memory. so call it standalone.
	 *       because of 'Automatic type inference', can ignore template type
	 * @param datas because of Copy, it can be local variable
	 */
	template <typename T>
	void copyVBODataToGPU(GLuint attribIndex, const CVector<T>& datas) {
		glBindBuffer(GL_ARRAY_BUFFER, m_vbo[attribIndex]);// When a buffer object is bound to a target, the previous binding for that target is automatically broken. 
		glBufferData(GL_ARRAY_BUFFER, datas.size() * sizeof(T), datas.data(), GL_STATIC_DRAW);
	}

	void copyIndexDataToGPU(const CVector<GLuint>& indices) {
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo[0]);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLuint), indices.data(), GL_STATIC_DRAW);
	}
	
	void checkCompileErrors(GLuint shader, int type, const CString& shaderName) {
		int success;
		char infoLog[1024];
		if (type != TYPE_PROGRAM) {
			glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
			if (!success) {
				glGetShaderInfoLog(shader, 1024, NULL, infoLog);
				std::string typeStr = "Unknow SHADER";
				switch(type) {
					case TYPE_VERTEX: typeStr = "VERTEX_SHADER"; break;
					case TYPE_FRAGMENT: typeStr = "FRAGMENT_SHADER"; break;
					case TYPE_GEOMETRY: typeStr = "GEOMETRY_SHADER"; break;
				}
				LogE << "#shaderName = " << shaderName << "\nERROR::SHADER_COMPILE_ERROR of: [" << type << "]\n" << infoLog;
			}
		} else {
			glGetProgramiv(shader, GL_LINK_STATUS, &success);
			if (!success) {
				glGetProgramInfoLog(shader, 1024, NULL, infoLog);
				LogE <<  "#shaderName = " << shaderName << "\nERROR::PROGRAM_LINK_ERROR :\n" << infoLog;
			}
		}
	}

	// not used
	void bindAttribIndexName(GLuint attribIndex, const char* attribName) {
		glBindAttribLocation(m_progID, attribIndex, attribName);
	}

private:
	void processIncludes(std::string& vertexCode, std::string& fragmentCode, std::string& geometryCode);
};
